<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <style>
        i,em{font-style: normal;}
        .select{width:200px;height:40px;line-height:40px;margin: 30px auto;position: relative;}
        .select span em{border-left: solid transparent 6px;border-right: solid transparent 6px;border-top: solid black 6px;position: absolute;right:10px;top:17px;}
        .select span{width:200px;height:40px;border: solid 1px black;display: block;}
        .select .list{border: solid 1px black; border-top: none;margin: 0;padding: 0;list-style: none;text-indent: 4px;width:200px;display: none;}
        .select .list li.active{background: red;color:#fff;}
        .select .list li em{float: right;margin-right:20px;}
    </style>
</head>
<body>

    <select>
        <option value="上海">上海</option>
        <option value="北京" selected>北京</option>
        <option value="深圳">深圳</option>
        <option value="杭州">杭州</option>
        <option value="南京">南京</option>
    </select>

    <div class="select">
        <span><i>默认显示区域</i><em></em></span>
        <ul class="list">
            <li><i>上海</i></li>
            <li><i>北京</i></li>
            <li><i>深圳</i></li>
            <li selected><i>杭州</i></li>
            <li><i>南京</i></li>
        </ul>
    </div>

</body>
<script>

    // 1. 选择器
    const ospan = document.querySelector(".select span");
    const oi = ospan.children[0];
    const oem = ospan.children[1];
    const olist = document.querySelector(".select .list");
    const ali = document.querySelectorAll(".select .list li");

    // 2. 准备状态，记录下拉菜单的显示或隐藏
    let state = 0;

    let index = null;

    // 6. 获取默认值
    for(let i=0;i<ali.length;i++){
        // 9. 给每个列表项添加索引
        ali[i].index = i;
        // 判断每个列表项身上是否存在默认选中的标志
        if(ali[i].attributes.selected){
            // 将该列表项的索引作为默认选中的索引
            index = i;
        }
    }
    // 7. 根据默认值，设置默认项
    setSelected(ali[index]);

    function setSelected(now){
        // 设置内容
        oi.innerHTML = now.children[0].innerHTML;
        // 设置选中的标志：√
        const em = document.createElement("em")
        em.innerText = "√";
        now.appendChild(em);
    }
    

    // 3. 点击显示区域
    ospan.onclick = function(eve){
        const e = eve || window.event;
        // 取消冒泡
        stopBubble(e);
        // 4. 根据状态，决定显示或隐藏下拉菜单
        if(state === 0){
            olist.style.display = "block";
            oem.style.transform = "rotate(180deg)";
            state = 1;
            // 18. 每次显示下拉菜单之前，先清除之前的操作
            if(lightIndex !== undefined){
                ali[lightIndex].className = "";
                lightIndex = undefined;
            }
        }else{
            olist.style.display = "none";
            oem.style.transform = "rotate(0deg)";
            state = 0;
        }
    }

    // 5. 点击空白，隐藏下拉菜单
    document.onclick = function(){
        olist.style.display = "none";
        oem.style.transform = "rotate(0deg)";
        state = 0;
    }

    // 8. 给每个li绑定鼠标进入离开点击事件
    for(let i=0;i<ali.length;i++){
        ali[i].onmouseover = function(){
            // 19. 鼠标进入列表项时，先清除可能存在的键盘操作的高亮项
            if(lightIndex !== undefined){
                ali[lightIndex].className = "";
            }
            // 设置高亮项
            this.className = "active";
            // 20. 将当前鼠标进入元素的索引，设置给键盘的高亮索引
            lightIndex = this.index;
        }
        ali[i].onmouseout = function(){
            // 隐藏高亮项
            this.className = "";
            // 21. 鼠标离开某个列表项，清除键盘高亮样式及索引
            if(lightIndex !== undefined){
                ali[lightIndex].className = "";
                lightIndex = undefined;
            }
        }
        ali[i].onclick = function(){
            // 删除上次选中项的标志
            ali[index].children[1].remove();
            // 设置本次选中项
            setSelected(this);
            // 10. 将本次选中项的索引，设置到默认索引，用来给下次清除上次
            index = this.index;
        }
    }
    // 11. 设置高亮项的默认索引
    let lightIndex = undefined;
    // 键盘事件
    document.onkeydown = function(eve){
        // 12. 下拉菜单隐藏时，键盘事件无效果
        if(state !== 1) return;
        const e = eve || window.event;
        const code = e.keyCode || e.which;
        // 上键
        if(code === 38){
            // 13. 判断是否是第一次按 上键
            if(lightIndex === undefined){
                // 高亮项索引为最后一个
                lightIndex = ali.length-1
            }
            // 14. 判断是否到 上键 的上限
            else if(lightIndex === 0){
                // 高亮项永远为上限值
                lightIndex = 0;
            }
            // 15. 正常高亮项索引计算
            else{
                // 修改高亮项索引之前，清除高亮样式
                ali[lightIndex].className = "";
                // 修改高亮项索引
                lightIndex--;
            }
            // 16. 根据高亮项索引，设置高亮样式
            ali[lightIndex].className = "active";
        }
        // 下键
        if(code === 40){
            if(lightIndex === undefined){
                lightIndex = 0;
            }else if(lightIndex === ali.length-1){
                lightIndex = ali.length-1;
            }else{
                ali[lightIndex].className = "";
                lightIndex++;
            }
            ali[lightIndex].className = "active";
        }
        // 回车
        if(code === 13){
            // 17. 确认选中，设置相关内容
            // 删除上次选中项的标志
            ali[index].children[1].remove();
            // 将键盘操作的高亮项索引设置给选中项的索引
            index = lightIndex;
            // 根据新选中项的索引，设置内容
            setSelected(ali[index]);
            // 隐藏下拉菜单
            olist.style.display = "none";
            // 旋转箭头
            oem.style.transform = "rotate(0deg)";
            // 修改下拉菜单的显示隐藏状态
            state = 0;
        }
    }



    function stopBubble(e){
        if(e.stopPropagation){
            e.stopPropagation();
        }else{
            e.cancelBubble = true;
        }
    }
</script>
</html>